/*
 *      Copyright 2012-2013 Battams, Derek
 *       
 *       Licensed under the Apache License, Version 2.0 (the "License");
 *       you may not use this file except in compliance with the License.
 *       You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *       Unless required by applicable law or agreed to in writing, software
 *       distributed under the License is distributed on an "AS IS" BASIS,
 *       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *       See the License for the specific language governing permissions and
 *       limitations under the License.
 */
package org.schedulesdirect.grabber;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.fluent.Request;
import org.apache.log4j.Appender;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.FileAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.SimpleLayout;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.schedulesdirect.api.Config;
import org.schedulesdirect.api.EpgClient;
import org.schedulesdirect.api.Headend;
import org.schedulesdirect.api.Message;
import org.schedulesdirect.api.NetworkEpgClient;
import org.schedulesdirect.api.UserStatus;
import org.schedulesdirect.api.ZipEpgClient;
import org.schedulesdirect.api.exception.InvalidCredentialsException;
import org.schedulesdirect.api.exception.InvalidZipCodeException;
import org.schedulesdirect.api.exception.ServiceOfflineException;
import org.schedulesdirect.utils.StringUtils;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;

import de.schlichtherle.truezip.file.TArchiveDetector;
import de.schlichtherle.truezip.file.TConfig;
import de.schlichtherle.truezip.file.TFile;
import de.schlichtherle.truezip.file.TFileOutputStream;
import de.schlichtherle.truezip.file.TVFS;
import de.schlichtherle.truezip.fs.archive.zip.JarDriver;
import de.schlichtherle.truezip.socket.sl.IOPoolLocator;

/**
 * An application that will download a Schedules Direct user's lineup data from the servers and generates a zip file in response
 * 
 * <p>This application can be used to generate a local cache file of a user's lineup data.  The generated cache file is suitable as a source for
 * the ZipEpgClient class.  That is, you can download a local file of your data and then use that file in your applications instead of always
 * having to contact the Schedules Direct servers for data.</p>
 * 
 * <p>This app can be used as a simple grabber tool if you just need to download the raw JSON data for your account.</p>
 * 
 * <p>To run this app, be sure to download the standalone jar from the project site then execute:</p>
 * 
 * <code>java -jar sdjson.jar</code>
 * 
 * <p>That command alone will dump all of the command line arguments required for operation.</p>
 * 
 * @author Derek Battams &lt;derek@battams.ca&gt;
 *
 */
public final class Grabber {
	static private Log LOG = null;
	
	/**
	 * Location of the saved command line args; saves typing on the command line
	 */
	static public final File OPTS_FILE = new File(new File(System.getProperty("user.home")), ".sd4j.opts");
	/**
	 * Name of the file holding headends in the zip
	 */
	static public final String HEADENDS_LIST = "headends.txt";
	/**
	 * Name of the file holding user data in the zip
	 */
	static public final String USER_DATA = "user.txt";
	/**
	 * Name of the file holding the zip file version number
	 */
	static public final String ZIP_VER_FILE = "version.txt";
	/**
	 * The zip file version this grabber generates
	 */
	static public final int ZIP_VER = 6;
	/**
	 * Max age of an airing before it's considered expired
	 */
	static public final long MAX_AIRING_AGE = 3L * 86400000L; // Ignore airings older than this
	/**
	 * Name of the "clean" logger used to display app output without level context
	 */
	static public final String LOGGER_APP_DISPLAY = "AppDisplay";
	
	static private JSONObject encodeHeadend(Headend he) throws JSONException {
		JSONObject o = new JSONObject();
		o.put(Headend.JSON_ID, he.getId());
		o.put(Headend.JSON_LOCATION, he.getLocation());
		o.put(Headend.JSON_NAME, he.getName());
		return o;
	}
	/**
	 * Has a download task failed?
	 */
	static volatile boolean failedTask = false;
	/**
	 * Regex of invalid chars for file names in the zip
	 */
	static public final String INVALID_FILE_CHARS = "[\\s\\\\\\/:\\*\\?\"<>\\|]";
	/**
	 * Scrub a file name, replacing invalid chars
	 * @param input The file name to scrub
	 * @return The scrubbed file name; suitable for use in the generated zip file
	 */
	static public String scrubFileName(String input) {
		return input.replaceAll(INVALID_FILE_CHARS, "_");
	}
	/**
	 * Supported actions for this app
	 * @author Derek Battams &lt;derek@battams.ca&gt;
	 *
	 */
	static public enum Action {
		GRAB,
		LIST,
		ADD,
		DELETE,
		INFO,
		SEARCH,
		CHPWD,
		AUDIT,
		LISTMSGS,
		DELMSG
	}
	
	private Collection<String> ignoreList = new ArrayList<String>();
	private Set<String> activeProgIds = new HashSet<String>();
	private JCommander parser;
	private GlobalOptions globalOpts;
	private CommandGrab grabOpts;
	private CommandList listOpts;
	private CommandAdd addOpts;
	private CommandDelete delOpts;
	private CommandChpwd chpwdOpts;
	private CommandSearch searchOpts;
	private CommandInfo infoOpts;
	private CommandAudit auditOpts;
	private CommandListMsgs listMsgsOpts;
	private CommandDeleteMsgs delMsgsOpts;
	private boolean freshZip;
	private long start;
	private boolean logosWarned = false;
	private Action action;
	
	private ThreadPoolExecutor pool;

	private void parseArgs(String[] args) {
		List<String> finalArgs = new ArrayList<String>();
		finalArgs.addAll(Arrays.asList(args));
		try {
			String savedUser = null;
			String savedPwd = null;
			if(OPTS_FILE.exists()) {
				Properties props = new Properties();
				Reader r = new FileReader(OPTS_FILE);
				props.load(r);
				r.close();
				savedUser = props.getProperty("user");
				savedPwd = props.getProperty("password");
			}
			globalOpts = new GlobalOptions(savedUser, savedPwd);
			parser = new JCommander(globalOpts);
			parser.setProgramName("sdjson");
			grabOpts = new CommandGrab();
			parser.addCommand("grab", grabOpts);
			listOpts = new CommandList();
			parser.addCommand("list", listOpts);
			addOpts = new CommandAdd();
			parser.addCommand("add", addOpts);
			delOpts = new CommandDelete();
			parser.addCommand("delete", delOpts);
//			chpwdOpts = new CommandChpwd();
//			parser.addCommand("chpwd", chpwdOpts);
			searchOpts = new CommandSearch();
			parser.addCommand("search", searchOpts);
			infoOpts = new CommandInfo();
			parser.addCommand("info", infoOpts);
			auditOpts = new CommandAudit();
			parser.addCommand("audit", auditOpts);
			listMsgsOpts = new CommandListMsgs();
			parser.addCommand("listmsgs", listMsgsOpts);
			delMsgsOpts = new CommandDeleteMsgs();
			parser.addCommand("delmsg", delMsgsOpts);
			parser.parse(finalArgs.toArray(new String[finalArgs.size()]));
			if(globalOpts.isHelp()) {
				parser.usage();
				System.exit(1);
			}
			if(LOG == null) {
				Appender a = null;
				File logFile = globalOpts.getLogFile();
				if(logFile != null)
					a = new FileAppender(new SimpleLayout(), logFile.getAbsolutePath(), false);
				else
					a = new ConsoleAppender(new SimpleLayout());
				Logger.getRootLogger().addAppender(a);
				Logger.getRootLogger().setLevel(globalOpts.getGrabberLogLvl());
				Logger.getLogger("org.apache.http").setLevel(globalOpts.getHttpLogLvl());
				LOG = LogFactory.getLog(Grabber.class);
				Logger l = Logger.getLogger(LOGGER_APP_DISPLAY);
				l.setAdditivity(false);
				l.setLevel(Level.ALL);
				Layout layout = new PatternLayout("%m");
				File consoleFile = globalOpts.getConsole();
				if(consoleFile != null)
					a = new FileAppender(layout, consoleFile.getAbsolutePath(), false);
				else
					a = new ConsoleAppender(layout);
				l.addAppender(a);
			}
		} catch(ParameterException e) {
			System.out.println(e.getMessage());
			String cmd = parser.getParsedCommand();
			if(cmd != null && !globalOpts.isHelp())
				parser.usage(cmd);
			else
				parser.usage();
			System.exit(1);			
		} catch(IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	private int deleteMessages(EpgClient clnt, Message[] msgs) throws IOException {
		int deleted = 0;
		for(String id : delMsgsOpts.getIds()) {
			for(Message m : msgs) {
				if(m.getId().equals(id)) {
					clnt.deleteMessage(m);
					++deleted;
					if(LOG.isInfoEnabled())
						LOG.info("Deleted message with id " + m.getId());
					break;
				}
			}
		}
		return deleted;
	}
	
	private void listAllMessages(EpgClient clnt) throws IOException {
		UserStatus status = clnt.getUserStatus();
		listMessages(clnt, status.getSystemMessages(), "SYSTEM MESSAGES\n===============");
		listMessages(clnt, status.getUserMessages(), "USER MESSAGES\n=============");
	}

	private void listMessages(EpgClient clnt, Message[] msgs, String header) throws IOException {
		Logger l = Config.get().getAppDisplay();
		if(msgs != null && msgs.length > 0) {
			l.info(header + "\n");
			SimpleDateFormat fmt = Config.get().getDateTimeFormat();
			for(Message m : msgs) {
				l.info(String.format("%-20s ID: %s%n", fmt.format(m.getDate()), m.getId()));
				l.info(String.format("\t%s%n", StringUtils.wrap(m.getContent(), 78, String.format("%n\t"), globalOpts.getConsole() != null)));
			}
		}
	}
	
	private void listHeadends(EpgClient clnt) throws IOException, InvalidZipCodeException {
		Logger l = Config.get().getAppDisplay();
		l.info(String.format("Available headends for user '%s'%n", globalOpts.getUsername()));
		l.info(String.format("%-12s Description%n==============================================================================%n", "Headend ID"));
		for(Headend he : clnt.getHeadends())
			l.info(String.format("%-12s %s %s%n", he.getId(), he.getName(), he.getLocation()));		
	}
	
	private void listHeadendsForZip(EpgClient clnt) throws IOException, InvalidZipCodeException {
		Logger l = Config.get().getAppDisplay();
		l.info(String.format("Available headends for zip '%s'%n", searchOpts.getPostalCode()));
		l.info(String.format("%-12s Description%n==============================================================================%n", "Headend ID"));
		for(Headend he : clnt.getHeadends(searchOpts.getPostalCode(), searchOpts.getIsoCountry()))
			l.info(String.format("%-12s %s %s%n", he.getId(), he.getName(), he.getLocation()));					
	}

	private void addHeadend(NetworkEpgClient clnt) {
		boolean oneFailure = false;
		for(String he : addOpts.getIds()) {
			try {
				clnt.addHeadendToAccount(he);
			} catch (IOException e) {
				oneFailure = true;
				LOG.error(String.format("Add headend command failed for '%s' [msg=%s]", he, e.getMessage()));
			}
		}
		if(oneFailure)
			System.exit(1);
		LOG.info("Headend(s) added successfully!");
	}

	private void removeHeadend(NetworkEpgClient clnt) {
		boolean oneFailure = false;
		for(String he : delOpts.getIds()) {
			try {
				clnt.removeHeadendFromAccount(he);
			} catch (IOException e) {
				oneFailure = true;
				LOG.error(String.format("Delete headend command failed for '%s' [msg=%s]", he, e.getMessage()));
			}
		}
		if(oneFailure)
			System.exit(1);
		LOG.info("Headend(s) deleted successfully!");
	}

	private void buildIgnoreList() {
		File ignoreFile = grabOpts.getIgnoreFile();
		if(ignoreFile != null && ignoreFile.canRead()) {
			try {
				ignoreList = FileUtils.readLines(ignoreFile, "UTF-8");
			} catch (IOException e) {
				LOG.error("IOError", e);
				ignoreList.clear();
			}
		}
	}

	private ThreadPoolExecutor createThreadPoolExecutor() {
		return new ThreadPoolExecutor(0, globalOpts.getMaxThreads(), 10, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadPoolExecutor.CallerRunsPolicy());
	}

	private void updateZip(NetworkEpgClient clnt) throws IOException, JSONException {
		Set<String> completedListings = new HashSet<String>();
		LOG.debug(String.format("Using %d worker threads", globalOpts.getMaxThreads()));
		pool = createThreadPoolExecutor(); 
		start = System.currentTimeMillis();
		File dest = grabOpts.getTarget();
		boolean rmDest = false;
		if(dest.exists()) {
			ZipEpgClient zipClnt = null;
			try {
				zipClnt = new ZipEpgClient(dest);
				if(!zipClnt.getUserStatus().getLastServerRefresh().before(clnt.getUserStatus().getLastServerRefresh())) {
					LOG.info("Current cache file contains latest data from Schedules Direct server; use --force-downlaod to force a new download from server.");
					boolean force = grabOpts.isForce();
					if(!force && !Config.get().isDebugOverrideActive())
						return;
					else if(!force)
						LOG.warn("Ignoring timestamp from Schedules Direct due to debug flag; proceeding with grab!");
					else
						LOG.warn("Forcing an update of data with the server due to user request!");
				}
			} catch(Exception e) {
				if(grabOpts.isKeep()) {
					LOG.error("Existing cache is invalid, keeping by user request!", e);
					return;
				} else {
					LOG.warn("Existing cache is invalid, deleting it; use --keep-bad-cache to keep existing cache!", e);
					rmDest = true;
				}
			} finally {
				if(zipClnt != null) try { zipClnt.close(); } catch(IOException e) {}
				if(rmDest && !dest.delete())
					throw new IOException("Unable to delete " + dest);
			}
		}

		freshZip = false;
		TFile vfs = null;
		if(!dest.exists()) { // We must create an initial zip to build from
			vfs = new TFile(dest);
			vfs.mkdir();
			OutputStream out = new TFileOutputStream(new TFile(dest, ZIP_VER_FILE));
			out.write(Integer.toString(ZIP_VER).getBytes("UTF-8"));
			out.close();
			freshZip = true;
		}
		ProgramCache progCache = ProgramCache.get(dest);
		JSONArray headends = new JSONArray();
		TFile lineups = new TFile(dest, "/lineups/");
		TFile scheds = new TFile(dest, "/schedules/");
		if(lineups.exists())
			lineups.rm_r();
		if(scheds.exists())
			scheds.rm_r();
		for(Headend he : clnt.getHeadends()) {
			String lineup = null;
			try {
				JSONArray jarr = new JSONArray();
				jarr.put(he.getId());
				JSONObject resp = new JsonRequest(clnt.getHash(), clnt.getUserAgent(), globalOpts.getUrl().toString()).submitForJson(JsonRequest.RequestAction.GET, JsonRequest.RequestObject.LINEUPS, jarr);
				if(!Utils.isErrorResponse(resp)) {
					String url;
					try {
						url = resp.getString("URL");
					} catch(JSONException e) {
						throw new IOException(e);
					}
					InputStream ins = Request.Get(url).execute().returnContent().asStream();
					ZipInputStream zip = new ZipInputStream(ins);
					ZipEntry ze = zip.getNextEntry();
					if(ze.getName().equals(EpgClient.SERVER_ID_ENTRY))
						zip.getNextEntry();
					lineup = IOUtils.toString(zip, "UTF-8");
					zip.close();
				}
			} catch(IOException e) {
				LOG.error(String.format("Error accessing lineup info for id '%s'", he.getId()), e);
				continue;
			}
			headends.put(encodeHeadend(he));
			Utils.writeVfsEntry(dest, String.format("lineups/%s.txt", scrubFileName(he.getId())), new JSONObject(lineup).toString(2));
			buildIgnoreList();
			JSONObject o = new JSONObject(lineup);
			JSONArray stations = o.getJSONArray("stations");
			JSONArray ids = new JSONArray();
			for(int i = 0; i < stations.length(); ++i) {
				JSONObject obj = stations.getJSONObject(i);
				String sid = obj.getString("stationID");
				if(ignoreList.contains(sid))
					LOG.debug(String.format("Skipped %s; found in ignore list", sid));
				else if(completedListings.add(sid)) {
					ids.put(sid);
					if(!grabOpts.isNoLogos()) {
						String callsign = obj.getString("callsign");
						if(logoCacheInvalid(obj, dest))
							pool.execute(new LogoTask(obj.getJSONObject("broadcaster"), callsign, dest));
						else
							LOG.debug(String.format("Skipped logo for %s; already cached!", callsign));
					} else if(!logosWarned) {
						logosWarned = true;
						LOG.warn("Logo downloads disabled by user request!");
					}
				} else
					LOG.debug(String.format("Skipped %s; already downloaded.", sid));
				if(ids.length() == grabOpts.getMaxSchedChunk()) {
					pool.execute(new ScheduleTask(ids, dest, clnt, progCache));
					ids = new JSONArray();
				}
			}
			if(ids.length() > 0)
				pool.execute(new ScheduleTask(ids, dest, clnt, progCache));
		}
		pool.shutdown();
		if(headends.length() > 0)
			Utils.writeVfsEntry(dest, HEADENDS_LIST, headends.toString(2));
		try {
			pool.awaitTermination(15, TimeUnit.MINUTES);
		} catch (InterruptedException e) {}

		pool = createThreadPoolExecutor();
		String[] dirtyPrograms = progCache.getDirtyIds();
		progCache.markAllClean();
		progCache = null;
		LOG.info(String.format("Identified %d program ids requiring an update!", dirtyPrograms.length));
		Collection<String> progIds = new ArrayList<String>();
		for(String progId : dirtyPrograms) {
			progIds.add(progId);
			if(progIds.size() == grabOpts.getMaxProgChunk()) {
				pool.execute(new ProgramTask(progIds, dest, clnt));
				progIds.clear();
			}
		}
		if(progIds.size() > 0)
			pool.execute(new ProgramTask(progIds, dest, clnt));
		pool.shutdown();
		try {
			pool.awaitTermination(15, TimeUnit.MINUTES);
		} catch (InterruptedException e) {}

		String userData = clnt.getUserStatus().toJson();
		if(failedTask) {
			LOG.error("One or more tasks failed!  Resetting last data refresh timestamp to zero.");
			SimpleDateFormat fmt = Config.get().getDateTimeFormat();
			String exp = fmt.format(new Date(0L));
			JSONObject o = new JSONObject(userData);
			o.put("lastDataUpdate", exp);
			userData = o.toString(2);
		}
		Utils.writeVfsEntry(dest, USER_DATA, userData);
		Utils.writeVfsEntry(dest, ZIP_VER_FILE, Integer.toString(ZIP_VER));
		removeIgnoredStations(dest);
	}
	
	private boolean logoCacheInvalid(JSONObject station, File vfs) throws JSONException, IOException {
		JSONObject o = station.optJSONObject("broadcaster");
		if(o != null) {
			JSONObject logo = o.optJSONObject("logo");
			if(logo != null) {
				String callsign = station.getString("callsign");
				String ext = logo.getString("URL");
				ext = ext.substring(ext.lastIndexOf('.') + 1);
				Date cached = Utils.getVfsEntryDate(vfs, String.format("logos/%s.%s", callsign, ext));
				if(cached != null) {
					Date lastMod;
					try {
						lastMod = Config.get().getDateTimeFormat().parse(logo.getString("modified"));
					} catch (ParseException e) {
						throw new IOException(e);
					}
					return cached.before(lastMod);
				}
			}
		}
		return true;
	}
	
	private void removeExpiredSchedules(File vfs) throws IOException, JSONException {
		int i = 0;
		for(TFile f : new TFile(vfs, "schedules").listFiles()) {
			String id = f.getName();
			id = id.substring(0, id.indexOf('.'));
			if(isScheduleExpired(new JSONArray(Utils.readVfsEntry(f)))) {
				LOG.debug(String.format("CacheCleaner: Expired '%s'", id));
				f.rm();
				++i;
			}
		}
		LOG.info(String.format("Removed %d expired schedule(s).", i));
	}

	private boolean isScheduleExpired(JSONArray sched) throws JSONException {
		boolean rc = true;
		Date expiry = new Date(System.currentTimeMillis() - (3L * 86400000L));
		for(int i = 0; i < sched.length(); ++i) {
			JSONObject air = sched.getJSONObject(i);
			if(!Utils.getEndDateForAiring(air).before(expiry)) {
				rc = false;
				activeProgIds.add(air.getString("programID"));
			}
		}
		return rc;
	}

	private void removeUnusedPrograms(File vfs) throws IOException {
		int i = 0;
		for(TFile f : new TFile(vfs, "programs").listFiles()) {
			String id = f.getName();
			id = id.substring(0, id.indexOf('.'));
			if(!activeProgIds.contains(id)) {
				LOG.debug(String.format("CacheCleaner: Unused '%s'", id));
				f.rm();
				++i;
			}				
		}
		LOG.info(String.format("Removed %d unused program(s).", i));
	}

	private void removeIgnoredStations(File vfs) throws IOException {
		int i = 0;
		for(TFile f : new TFile(vfs, "schedules").listFiles()) {
			String id = f.getName();
			id = id.substring(0, id.indexOf('.'));
			if(ignoreList.contains(id)) {
				LOG.debug(String.format("CacheCleaner: Remove '%s'", id));
				f.rm();
				++i;
			}				
		}
		LOG.info(String.format("Removed %d ignored station(s).", i));
	}

	/**
	 * Execute the grabber app
	 * @param args The command line args
	 * @throws IOException Thrown on any unexpected IO error
	 * @throws InvalidCredentialsException Thrown if the login attempt to Schedules Direct failed
	 * @throws InvalidZipCodeException Thrown if the given zip code was invalid
	 */
	public void run(String[] args) throws IOException, InvalidCredentialsException, InvalidZipCodeException {
		parseArgs(args);
		if(parser.getParsedCommand() == null) {
			parser.usage();
			System.exit(1);
		}
		NetworkEpgClient clnt = null;
		try {
			if(!parser.getParsedCommand().equals("audit")) {
				try {
					clnt = new NetworkEpgClient(globalOpts.getUsername(), globalOpts.getPassword(), globalOpts.getUserAgent(), globalOpts.getUrl().toString(), true);
					LOG.debug(String.format("Client details: %s", clnt.getUserAgent()));
				} catch(ServiceOfflineException e) {
					LOG.error("Web service is offline!");
					if(!Config.get().isDebugOverrideActive()) {
						LOG.error("Please try again later.");
						System.exit(1);
					} else
						LOG.warn("Ignoring web service status due to debug mode override!");
				}
			}
			action = Action.valueOf(parser.getParsedCommand().toUpperCase());
			switch(action) {
				case LIST:
					if(!listOpts.isHelp())
						listHeadends(clnt);
					else
						parser.usage(action.toString().toLowerCase());
					break;
				case GRAB:
					if(!grabOpts.isHelp())
						updateZip(clnt);
					else
						parser.usage(action.toString().toLowerCase());
					break;
				case ADD: 
					if(!addOpts.isHelp())
						addHeadend(clnt);
					else
						parser.usage(action.toString().toLowerCase());
					break;
				case DELETE: 
					if(!delOpts.isHelp())
						removeHeadend(clnt);
					else
						parser.usage(action.toString().toLowerCase());
					break;
				case INFO: 
					if(!infoOpts.isHelp())
						dumpAccountInfo(clnt);
					else
						parser.usage(action.toString().toLowerCase());
					break;
				case CHPWD: 
					if(!chpwdOpts.isHelp())
						changePassword(clnt);
					else
						parser.usage(action.toString().toLowerCase());
					break;
				case SEARCH:
					if(!searchOpts.isHelp())
						listHeadendsForZip(clnt);
					else
						parser.usage(action.toString().toLowerCase());
					break;
				case AUDIT:
					if(!auditOpts.isHelp()) {
						Auditor a = new Auditor(auditOpts);
						a.run();
						if(a.isFailed()) {
							LOG.error("Auditor failed!");
							System.exit(1);
						}
					} else
						parser.usage(action.toString().toLowerCase());
					break;
				case LISTMSGS:
					if(!listMsgsOpts.isHelp())
						listAllMessages(clnt);
					else
						parser.usage(action.toString().toLowerCase());
					break;
				case DELMSG:
					if(!delMsgsOpts.isHelp()) {
						if(deleteMessages(clnt, clnt.getUserStatus().getSystemMessages()) < delMsgsOpts.getIds().size())
							deleteMessages(clnt, clnt.getUserStatus().getUserMessages());
					} else
						parser.usage(action.toString().toLowerCase());
					break;
			}
		} catch(ParameterException e) {
			System.out.println(e.getMessage());
			parser.usage();
			System.exit(1);
		} catch(JSONException e) {
			throw new RuntimeException(e);
		} finally {
			if(clnt != null)
				clnt.close();
			File target = grabOpts.getTarget();
			if(action == Action.GRAB && !grabOpts.isHelp() && grabOpts.isPurge() && !freshZip) {
				LOG.warn("Performing a cache cleanup, this will take a few minutes!");
				try {
					removeExpiredSchedules(target);
				} catch(JSONException e) {
					throw new IOException(e);
				}
				removeUnusedPrograms(target);
			}
			TVFS.umount();
			if(action == Action.GRAB && !grabOpts.isHelp())
				LOG.info(String.format("Created '%s' successfully! [%dms]", target, System.currentTimeMillis() - start));
			if(globalOpts.isSaveCreds()) {
				String user = globalOpts.getUsername();
				String pwd = globalOpts.getPassword();
				if(user != null && user.length() > 0 && pwd != null && pwd.length() > 0) {
					Properties props = new Properties();
					props.setProperty("user", globalOpts.getUsername());
					props.setProperty("password", globalOpts.getPassword());
					Writer w = new FileWriter(OPTS_FILE);
					props.store(w, "Generated by sd4j");
					w.close();
					LOG.info(String.format("Credentials and headends saved for future use in %s!", OPTS_FILE.getAbsoluteFile()));
				}
			}
		}
	}

	private void dumpAccountInfo(NetworkEpgClient clnt) throws IOException {
		System.out.println(clnt.getUserStatus());
	}
	
	private void changePassword(NetworkEpgClient clnt) throws IOException {
		clnt.changePassword(chpwdOpts.getNewPassword());
	}
	
	public static void main(String[] args) throws Exception {
		TConfig.get().setArchiveDetector(new TArchiveDetector("epg", new JarDriver(IOPoolLocator.SINGLETON)));
		new Grabber().run(args);
	}
}
